package x.ovo.wechat.bot.impl.plugin;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.dromara.hutool.core.array.ArrayUtil;
import org.dromara.hutool.core.io.file.FileUtil;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.json.JSON;
import org.dromara.hutool.json.JSONArray;
import org.dromara.hutool.json.JSONObject;
import org.dromara.hutool.json.JSONUtil;
import picocli.CommandLine;
import x.ovo.wechat.bot.core.Constant;
import x.ovo.wechat.bot.core.Context;
import x.ovo.wechat.bot.core.command.CommandExcutor;
import x.ovo.wechat.bot.core.exception.PluginException;
import x.ovo.wechat.bot.core.plugin.Plugin;
import x.ovo.wechat.bot.core.plugin.PluginDescription;
import x.ovo.wechat.bot.core.plugin.PluginManager;

import java.io.File;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 默认插件管理器
 *
 * @author ovo on 2024/07/09.
 */
@SuppressWarnings("DuplicatedCode")
public enum DefaultPluginManager implements PluginManager {
    INSTANCE;

    /** 上下文 */
    @Getter
    private final Context context = Context.INSTANCE;
    /** 插件容器 */
    private final Map<String, Plugin> plugins = new HashMap<>();
    /** 群插件限制 */
    private final Map<String, Set<Plugin>> limit = new HashMap<>();
    /** 限制模式 */
    private Mode mode = Mode.WHITE_LIST;

    @RequiredArgsConstructor
    enum Mode {
        BLACK_LIST("black-list"),
        WHITE_LIST("white-list");

        private final String name;

        public static Mode of(String name) {
            return Arrays.stream(values()).filter(mode -> mode.name.equals(name)).findFirst().orElseThrow(() -> new PluginException("未知的限制模式：" + name));
        }
    }

    /**
     * 加载全部插件
     */
    private void loadAll() {
        // 遍历plugins目录下的所有jar文件
        File[] files = Constant.Files.PLUGIN_DIR.listFiles((dir, name) -> name.endsWith(".jar"));
        if (ArrayUtil.isNotEmpty(files)) {
            Stream.of(files).forEach(file -> {
                try {
                    this.load(file);
                } catch (Exception e) {
                    log.error("插件 {} 加载失败", file.getName(), e);
                }
            });
            log.info("载入了 {} 个插件", this.plugins.size());
        }
    }


    @Override
    public void load(String pluginName) {
        File[] files = Constant.Files.PLUGIN_DIR.listFiles((dir, name) -> name.endsWith(".jar"));
        if (ArrayUtil.isNotEmpty(files)) {
            File file = Stream.of(files)
                    .filter(f -> pluginName.equals(PluginLoader.getDescription(f).getName()))
                    .findAny()
                    .orElseThrow(() -> new PluginException(StrUtil.format("插件 [{}] 加载失败，未能查找到指定插件jar", pluginName)));
            this.load(file);
            log.info("插件 [{}] 已成功加载", pluginName);
        }
    }

    @Override
    public void load(File file) {
        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            PluginDescription description = PluginLoader.getDescription(file);
            Assert.notNull(description, () -> new PluginException(StrUtil.format("插件 [{}] 加载失败，插件描述文件不存在", file.getName())));
            Assert.isFalse(this.plugins.containsKey(description.getName()), () -> new PluginException(StrUtil.format("插件 [{}] 加载失败，插件 [{}] 已存在", file.getName(), description.getName())));

            // 获取插件主类
            Plugin plugin = PluginLoader.load(file, description);
            log.debug("[{}] ({}) {} - {}", description.getName(), plugin.getPriority(), description.getVersion(), description.getDescription());
            plugin.setContext(this.context);
            // 调用插件加载回调
            plugin.onLoad();
            // 将插件存入容器
            this.plugins.put(description.getName(), plugin);
            // 获取插件中的事件监听器，注册到事件管理器中
            this.context.getEventManager().register(plugin);
            // 注册命令执行器
            this.context.getCommandManager().register(plugin);
            log.debug("[{}] {}", description.getName(), plugin.isEnabled() ? "已启用" : "未启用");
//        } catch (Exception e) {
//            log.error("插件 {} 加载失败", file.getName(), e);
        } finally {
            // 恢复原类加载器
            Thread.currentThread().setContextClassLoader(originalClassLoader);
        }
    }

    @Override
    public void unload(String name) {
        Plugin plugin = Assert.notNull(this.plugins.get(name), () -> new PluginException(StrUtil.format("插件 [{}] 不存在", name)));
        this.unload(plugin);
    }

    @Override
    @SneakyThrows
    public void unload(Plugin plugin) {
        plugin.onUnload();
        this.plugins.remove(plugin.getPluginDesc().getName());
        this.context.getEventManager().unregister(plugin);
        CommandExcutor excutor = plugin.getCommandExcutor();
        Optional.ofNullable(excutor).ifPresent(e -> this.context.getCommandManager().unregister(new CommandLine(e).getCommandName()));
        ((URLClassLoader) plugin.getClassLoader()).close();
        log.info("卸载插件 {} 成功", plugin.getPluginDesc().getName());
    }

    @Override
    public void enable(String name, String from) {
        Plugin plugin = Assert.notNull(this.plugins.get(name), () -> new PluginException(StrUtil.format("插件 [{}] 不存在", name)));
        // 如果指令来源的用户名为空，则启用插件，否则将插件添加到限制map中
        if (StrUtil.isBlank(from)) {
            plugin.setEnabled(true);
            return;
        }

        Set<Plugin> plugins = this.limit.computeIfAbsent(from, k -> new HashSet<>());
        if (this.mode == Mode.WHITE_LIST) {
            plugins.add(plugin);
        } else {
            plugins.remove(plugin);
        }
    }

    @Override
    public void disable(String name, String from) {
        Plugin plugin = Assert.notNull(this.plugins.get(name), () -> new PluginException(StrUtil.format("插件 [{}] 不存在", name)));
        // 如果指令来源的用户名为空，则禁用插件，否则将插件从限制map中移除
        if (StrUtil.isBlank(from)) {
            plugin.setEnabled(false);
            return;
        }

        Set<Plugin> plugins = this.limit.computeIfAbsent(from, k -> new HashSet<>());
        if (this.mode == Mode.WHITE_LIST) {
            plugins.remove(plugin);
        } else {
            plugins.add(plugin);
        }
    }

    @Override
    public boolean isLimit(String name, String from) {
        // 检查是否在名单内
        boolean contains = this.limit.getOrDefault(from, Collections.emptySet()).contains(this.plugins.get(name));
        // 白名单模式下，不在名单内即为限制
        return (this.mode == Mode.WHITE_LIST) != contains;
    }

    @Override
    public Plugin get(String name) {
        return Assert.notNull(this.plugins.get(name), () -> new PluginException(StrUtil.format("插件 [{}] 不存在", name)));
    }

    @Override
    public Set<Plugin> list() {
        return new HashSet<>(this.plugins.values());
    }

    @Override
    public Set<Plugin> list(String from) {
        return this.limit.getOrDefault(from, Collections.emptySet()).stream().filter(Plugin::isEnabled).collect(Collectors.toSet());
    }

    @Override
    public void saveLimitConfig() {
        Map<String, Set<String>> map = this.limit.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        e -> e.getValue().stream().map(Plugin::getPluginDesc).map(PluginDescription::getName).collect(Collectors.toSet())
                ));

        JSONObject object = JSONUtil.ofObj().set("limit", map).set("mode", this.mode.name);
        FileUtil.writeBytes(JSONUtil.toJsonPrettyStr(object).getBytes(), Constant.Files.GROUP_PLUGIN_FILE);
        log.info("群插件配置写入完成");
    }

    @Override
    public void init() {
        PluginManager.super.init();
        // 载入插件
        this.loadAll();
        // 判断群插件配置文件是否存在，如果存在则加载
        if (Constant.Files.GROUP_PLUGIN_FILE.exists()) {
            JSON json = JSONUtil.readJSON(Constant.Files.GROUP_PLUGIN_FILE, StandardCharsets.UTF_8);
            Map<String, Set<Plugin>> collect = JSONUtil.parseObj(json).getJSONObject("limit").entrySet().stream()
                    .collect(Collectors.toMap(
                            Map.Entry::getKey,
                            e -> ((JSONArray) e.getValue()).stream().map(this.plugins::get).filter(Objects::nonNull).collect(Collectors.toSet())
                    ));

            this.mode = Mode.of(JSONUtil.parseObj(json).getStr("mode"));
            this.limit.putAll(collect);
            log.debug("群插件限制模式：{}，限制名单：{}", this.mode, this.limit.entrySet().stream().map(e -> StrUtil.format("{}：{}", e.getKey(), e.getValue().stream().map(p -> p.getPluginDesc().getName()).toList())).toList());
            log.info("群插件配置载入完成，限制模式：{}", this.mode);
        }
    }

}
